﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;

using hellaSharp;
using CookComputing.XmlRpc;

using agsXMPP;
using agsXMPP.protocol;
using agsXMPP.protocol.client;
using agsXMPP.protocol.iq.disco;
using agsXMPP.protocol.extensions.caps;
using agsXMPP.protocol.extensions.si;
using agsXMPP.protocol.extensions.filetransfer;
using agsXMPP.protocol.extensions.featureneg;
using agsXMPP.protocol.extensions.bytestreams;
using agsXMPP.protocol.x.data;

using agsXMPP.Xml;

namespace hellaBot
{
    class Jabber
    {
        private XmppClientConnection conn;
        private Jid jid;
        private agsXMPP.protocol.x.muc.MucManager mucManager;
        private HellaSharp myhella = new HellaSharp(Program.configuration.hellanzbxmlurl, Program.configuration.hellanzbpass);

        public Jabber(string server) // Create a connection to specified server
        {
            conn = new agsXMPP.XmppClientConnection(server);
            conn.EnableCapabilities = true;
            conn.Capabilities.AddExtension("filexfer");
            conn.Capabilities.Node = "http://www.ag-software.de/miniclient/caps";
            conn.ClientVersion = "0.1";
        }
        public Jabber(string server, int port) // Create a connection to specified server on specified port
        {
            conn = new agsXMPP.XmppClientConnection(server, port);
            conn.EnableCapabilities = true;
            conn.Capabilities.AddExtension("filexfer");
            conn.Capabilities.Node = "http://www.ag-software.de/miniclient/caps";
            conn.ClientVersion = "0.1";
        }
        public Jabber(string server, int port, string user, string password, string resource) // Connect and login
        {
            jid = new Jid(user, server, resource);
            conn = new XmppClientConnection(jid.Server, port);
            conn.EnableCapabilities = true;
            conn.Capabilities.AddExtension("filexfer");
            conn.Capabilities.Node = "http://www.ag-software.de/miniclient/caps";
            conn.ClientVersion = "0.1";
        }

        // Perform all the prelogin stuff and trigger the login (which happens async)
        public bool Login(string user, string pass, string resource)
        {
            jid = new Jid(user);
            jid.Resource = resource;

            conn.Password = pass;
            conn.Username = jid.User;

            conn.AutoAgents = false;
            conn.AutoPresence = true;
            conn.AutoRoster = true;
            conn.AutoResolveConnectServer = true;

            // Connect to the server now 
            // !!! this is asynchronous !!!
            try
            {
                conn.OnRosterStart += new ObjectHandler(xmppCon_OnRosterStart);
                conn.OnRosterItem += new XmppClientConnection.RosterHandler(xmppCon_OnRosterItem);
                conn.OnRosterEnd += new ObjectHandler(xmppCon_OnRosterEnd);
                conn.OnPresence += new PresenceHandler(xmppCon_OnPresence);
                conn.OnMessage += new MessageHandler(xmppCon_OnMessage);
                conn.OnLogin += new ObjectHandler(xmppCon_OnLogin);
                conn.OnIq += new IqHandler(xmppCon_OnIq);
                conn.OnAuthError += new XmppElementHandler(xmppCon_OnAuthError);

                conn.Open();
            }
            catch (Exception e)
            {
                Terminal.PrintError("Connection to server failed: " + e.Message);
                return false;
            }

            return true;
        }

        private void xmppCon_OnAuthError(object sender, agsXMPP.Xml.Dom.Element e) // login failed
        {
            Program.Die("Login failed to Jabber"); // interrupt main thread causing program to end
        }

        private void xmppCon_OnIq(object sender, agsXMPP.protocol.client.IQ iq) // onIq specifically handles filetransfer IQs
        {
            if (iq != null)
            {
                // No Iq with query
                if (iq.HasTag(typeof(agsXMPP.protocol.extensions.si.SI)))
                {
                    if (iq.Type == IqType.set)
                    {
                        agsXMPP.protocol.extensions.si.SI si = iq.SelectSingleElement(typeof(agsXMPP.protocol.extensions.si.SI)) as agsXMPP.protocol.extensions.si.SI;

                        agsXMPP.protocol.extensions.filetransfer.File file = si.File;
                        if (file != null)
                        {
                            if (!isUser(iq.From))
                            {
                                Terminal.PrintError("File from " + iq.From.ToString() + " refused (is not user on this bot");
                                Say(iq.From.ToString(), "This is not a public service, you must be registered to use this bot");
                            }
                            // somebody wants to send a file to us
                            // Console.WriteLine(file.Size.ToString());
                            // Console.WriteLine(file.Name);
                            // frmFileTransfer frmFile = new frmFileTransfer(XmppCon, iq);
                            // frmFile.Show();
                            FileTransfer ft = new FileTransfer(conn, iq);        // Set off the dedicated FileTransfer process                       
                        }
                    }
                }
                else
                {
                    agsXMPP.Xml.Dom.Element query = iq.Query;

                    if (query != null)
                    {
                        Type mytype = query.GetType();
                        if (query.GetType() == typeof(agsXMPP.protocol.iq.version.Version))
                        {
                            // its a version IQ VersionIQ
                            agsXMPP.protocol.iq.version.Version version = query as agsXMPP.protocol.iq.version.Version;
                            if (iq.Type == IqType.get)
                            {
                                // Somebody wants to know our client version, so send it back
                                iq.SwitchDirection();
                                iq.Type = IqType.result;

                                version.Name = "hellabot";
                                version.Ver = "0.1";
                                version.Os = Environment.OSVersion.ToString();

                                conn.Send(iq);
                            }
                        }
                        else if (query.GetType() == typeof(agsXMPP.protocol.iq.disco.DiscoInfo)) // Request for capabilities
                        {
                            DiscoInfoIq diiq = new DiscoInfoIq();
                            diiq.Id = "disco1";
                            diiq.To = iq.From;
                            diiq.Type = agsXMPP.protocol.client.IqType.result;

                            diiq.Query.AddIdentity(new DiscoIdentity("pc", "client"));

                            diiq.Query.AddFeature(new DiscoFeature(agsXMPP.Uri.DISCO_INFO));
                            diiq.Query.AddFeature(new DiscoFeature(agsXMPP.Uri.DISCO_ITEMS));
                            diiq.Query.AddFeature(new DiscoFeature(agsXMPP.Uri.MUC));
                            diiq.Query.AddFeature(new DiscoFeature(agsXMPP.Uri.SI_FILE_TRANSFER));  // This is important, this says we accept file transfers

                            // Program.Print(diiq);

                            // Build caps from this disco info
                            Capabilities caps = new Capabilities();
                            caps.Node = "http://www.ag-software.de/miniclient/caps";
                            caps.SetVersion(diiq.Query);

                            conn.Send(diiq);
                        }
                    }
                }
            }
        }

        private void xmppCon_OnLogin(object sender) // Successful login, create a MUC manager for later use
        {
            Terminal.PrintMessage("Logged in successfully");
            mucManager = new agsXMPP.protocol.x.muc.MucManager(conn);
            PrintCommands(); // Print commands to console
        }

        private void xmppCon_OnRosterEnd(object sender)
        {
            // _bWait = false; // Significance of this? :)            
            // Terminal.PrintMessage("Received roster from server");
        }

        private void xmppCon_OnRosterItem(object sender, agsXMPP.protocol.iq.roster.RosterItem item)
        {
            // Terminal.PrintMessage("Got a new contact: " + item.Jid);
        }

        private void xmppCon_OnRosterStart(object sender)
        {
            // Terminal.PrintMessage("Requesting contacts");
        }

        private void xmppCon_OnPresence(object sender, Presence pres)
        {
            /* PrintInfo(String.Format("Got presence from: {0}", pres.From.ToString()));
            PrintInfo(String.Format("type: {0}", pres.Type.ToString()));
            PrintInfo(String.Format("status: {0}", pres.Status));
            PrintInfo("");*/
        }

        private void xmppCon_OnMessage(object sender, Message msg)
        {
            if (!isUser(msg.From))
            {
                Terminal.PrintError("Message from " + msg.From.ToString() + " refused (is not user on this bot");
                Say(msg.From, "This is not a public service, you must be registered to use this bot your message wil be ignored - sorry :)", msg.Type);
                return;
            }
            if (msg.Body != null)
            {
                /*PrintEvent(String.Format("Got message from: {0}", msg.From.ToString()));
                PrintEvent("message: " + msg.Body);
                PrintInfo("");*/
                Terminal.PrintMessage("Got message from " + msg.From.ToString() + ": " + msg.Body);
                if (msg.Body.StartsWith("!"))
                    ParseCommand(msg.From, msg.Body, msg.Type);
            }
        }

        private void ParseCommand(Jid from, string command, MessageType type) // Parse a command from Jabber message
        {
            command = command.Substring(1); // Strip the ! that identified this as a command
            string[] parts;
            char[] splitter = { ' ' };
            parts = command.Split(splitter, 3); // Break into parts
            XmlRpcStruct output;     
            object[] log;
            object[] currentDownload;            
            object[] queuedDownload;
            object[] processingDownload;

            switch (parts[0]) // !parts[0] parts[1] // [0] is the command
            {
                default: // No hit
                    Say(from.Bare, "Unrecognized command");
                    break;                
                case "enqueue":
                    if (parts.Length != 2)
                        Terminal.PrintError("Syntax invalid try newzbin <id>");
                    else
                    {
                        if (!System.Text.RegularExpressions.Regex.IsMatch(parts[1], @"[0-9]{7}"))
                        {
                            Say(from, "That didn't look like a newzbin id, sorry", type);
                            return;
                        }
                        try
                        {
                            output = myhella.EnqueueId(parts[1]); // Send request to hella interface
                            Say(from, "Queued request for " + parts[1] + " to see the current state try !status or !log (<n>)", type);
                        }
                        catch (Exception e)
                        {
                            Say(from, "Unable to queue request for "+ parts[1] +", sorry!", type);
                        }
                    }
                    break;
                case "pause":
                    try
                    {
                        output = myhella.Pause();
                        currentDownload = (object[])output["currently_downloading"];
                        foreach (object i in currentDownload)
                        {
                            XmlRpcStruct tmp = (XmlRpcStruct)i;
                            Say(from, "Paused " + tmp["nzbName"].ToString() + " @ " + (String)output["percent_complete"].ToString() + "%", type);
                        }
                    }
                    catch (Exception e)
                    {
                        Say(from, "Exception: " + e.Message, type);
                    }
                    break;
                case "continue":
                    output = myhella.Continue();
                    currentDownload = (object[])output["currently_downloading"];
                    foreach (object i in currentDownload)
                    {
                        XmlRpcStruct tmp;
                        tmp = (XmlRpcStruct)i;
                        Say(from, "Unpaused " + tmp["nzbName"].ToString() + " @ " + (String)output["percent_complete"].ToString() + "%", type);
                    }
                    break;
                case "status":
                    output = myhella.Status();
                    log = (object[])output["log_entries"];
                    currentDownload = (object[])output["currently_downloading"];
                    queuedDownload = (object[])output["queued"];
                    processingDownload = (object[])output["currently_processing"];

                    try
                    {

                        foreach (object i in currentDownload)
                        {
                            XmlRpcStruct tmp;
                            tmp = (XmlRpcStruct)i;
                            Say(from, "Downloading: " + tmp["nzbName"].ToString() + " - " + output["percent_complete"].ToString() + "% complete @ " + output["rate"].ToString() + "kbps", type);
                        }

                        foreach (object i in processingDownload)
                        {
                            XmlRpcStruct tmp;
                            tmp = (XmlRpcStruct)i;
                            Say(from, "Processing: " + tmp["nzbName"].ToString(), type);
                        }

                        foreach (object i in queuedDownload)
                        {
                            XmlRpcStruct tmp;
                            tmp = (XmlRpcStruct)i;
                            Say(from, "Queued: " + tmp["nzbName"].ToString(), type);
                        }

                        if (type == MessageType.groupchat)
                            Say(from, "Check PM for any log output", type);

                        foreach (object i in log)
                        {
                            XmlRpcStruct tmp;
                            tmp = (XmlRpcStruct)i;                            
                            
                            Say(from, (String)tmp["ERROR"] + (String)tmp["INFO"], MessageType.chat);
                        }
                    }
                    catch (Exception e)
                    {
                        Terminal.PrintError(e.Message);
                    }
                    break;

                case "log":
                    output = myhella.Status();
                    log = (object[])output["log_entries"];
                    log.Reverse();
                    int lines = 10;
                    int c = 0;
                    if (parts.Length == 2)
                        lines = Convert.ToInt32(parts[1]);

                    foreach (object i in log)
                    {
                        XmlRpcStruct tmp;
                        tmp = (XmlRpcStruct)i;

                        Say(from, (String)tmp["ERROR"] + (String)tmp["INFO"], MessageType.chat);

                        c++;

                        if (c == lines)
                            break;
                    }
                    break;
                case "help":
                    Say(from, @"Available commands on this bot: enqueue, pause, continue, status, log, help", type);
                    break;
            }
        }

        public bool JoinMuc(string mucName)
        {
            Jid muc = new Jid(mucName);
            try
            {
                mucManager.JoinRoom(muc, "collaBot");
            }
            catch (Exception e)
            {
                Terminal.PrintError("Failed to join conference room");
                return false;
            }
            return true;
        }

        public bool Say(string who, string message)
        {
            Jid target = new Jid(who);
            MessageType type = MessageType.normal;
            if (who.Contains("conference"))
                type = MessageType.groupchat;

            try
            {
                conn.Send(new Message(target, type, message));
            }
            catch (Exception e)
            {
                return false;
            }
            return true;
        }

        public bool Say(Jid target, string message, MessageType type)
        {
            try
            {
                if (type == MessageType.chat)
                    conn.Send(new Message(target, type, message));
                else if (type == MessageType.groupchat)
                    conn.Send(new Message(new Jid(target.Bare), type, message));
            }
            catch (Exception e)
            {
                return false;
            }
            return true;
        }

        private void PrintCommands()
        {
            Terminal.PrintMessage("------------------------------");
            Terminal.PrintMessage("Welcome, the bot is now online");
            Terminal.PrintMessage("Available commands:");
            Terminal.PrintMessage("join <muc>");
            Terminal.PrintMessage("say jid message");
            Terminal.PrintMessage("------------------------------");
        }

        private bool isUser(Jid user)
        {
            return Program.configuration.users.Contains(new Jid(user.Bare));
        }
    }
}
